feat: Transfer protocol for TypeGPU resources (on souls) - #2797
Conversation
|
pkg.pr.new packages benchmark commit |
Resolution Time Benchmark---
config:
themeVariables:
xyChart:
plotColorPalette: "#E63946, #3B82F6, #059669"
---
xychart
title "Random Branching (🔴 PR | 🔵 main | 🟢 release)"
x-axis "max depth" [1, 2, 3, 4, 5, 6, 7, 8]
y-axis "time (ms)"
line [0.90, 1.86, 3.97, 6.06, 7.12, 11.95, 21.98, 23.34]
line [0.87, 1.93, 3.86, 5.60, 6.61, 11.70, 21.59, 25.48]
line [0.90, 1.82, 4.44, 6.12, 6.90, 11.63, 22.04, 22.72]
---
config:
themeVariables:
xyChart:
plotColorPalette: "#E63946, #3B82F6, #059669"
---
xychart
title "Linear Recursion (🔴 PR | 🔵 main | 🟢 release)"
x-axis "max depth" [1, 2, 3, 4, 5, 6, 7, 8]
y-axis "time (ms)"
line [0.32, 0.49, 0.67, 0.78, 1.11, 1.13, 1.42, 1.61]
line [0.32, 0.52, 0.70, 0.85, 1.12, 1.16, 1.44, 1.53]
line [0.28, 0.54, 0.67, 0.89, 1.23, 1.27, 1.43, 1.60]
---
config:
themeVariables:
xyChart:
plotColorPalette: "#E63946, #3B82F6, #059669"
---
xychart
title "Full Tree (🔴 PR | 🔵 main | 🟢 release)"
x-axis "max depth" [1, 2, 3, 4, 5, 6, 7, 8]
y-axis "time (ms)"
line [0.80, 2.13, 3.38, 7.24, 12.52, 24.78, 53.22, 110.00]
line [0.69, 2.19, 4.32, 6.64, 12.64, 24.38, 53.16, 107.51]
line [0.81, 1.99, 4.22, 6.28, 12.50, 25.61, 54.53, 113.18]
|
Bundle size comparison (
|
| 🟢 Decreased | ➖ Unchanged | 🔴 Increased (max 6.16%) | ❔ Unknown |
|---|---|---|---|
| 0 | 2 | 320 | 1 |
import * as ... in PR vs import * as ... in target (did bundle size increase?):
Click to reveal the results table (119 entries).
| Test | tsdown |
|---|---|
| d_unstruct.ts | 1.65 kB ( |
| d_Void.ts | 759 B ( |
| d_sampler.ts | 767 B ( |
| d_isPtr.ts | 777 B ( |
| d_comparisonSampler.ts | 778 B ( |
| d_isSizeAttrib.ts | 779 B ( |
| d_isWgslArray.ts | 779 B ( |
| d_isAlignAttrib.ts | 780 B ( |
| d_isAtomic.ts | 780 B ( |
| d_isWgslStruct.ts | 780 B ( |
| d_isBuiltinAttrib.ts | 782 B ( |
| d_isDecorated.ts | 783 B ( |
| d_isLocationAttrib.ts | 783 B ( |
| d_isInterpolateAttrib.ts | 786 B ( |
| d_atomic.ts | 804 B ( |
| d_ptrHandle.ts | 876 B ( |
| d_ptrUniform.ts | 877 B ( |
| d_ptrStorage.ts | 881 B ( |
| d_ptrPrivate.ts | 883 B ( |
| d_ptrFn.ts | 884 B ( |
| d_ptrWorkgroup.ts | 885 B ( |
| d_textureExternal.ts | 898 B ( |
| d_textureStorage1d.ts | 1.04 kB ( |
| d_textureStorage2d.ts | 1.04 kB ( |
| d_textureStorage3d.ts | 1.04 kB ( |
| d_textureStorage2dArray.ts | 1.05 kB ( |
| d_struct.ts | 3.71 kB ( |
| d_isDisarray.ts | 1.14 kB ( |
| d_isUnstruct.ts | 1.14 kB ( |
| d_isLooseDecorated.ts | 1.14 kB ( |
| d_isLooseData.ts | 1.18 kB ( |
| d_isWgslData.ts | 1.33 kB ( |
| d_ref.ts | 4.99 kB ( |
| d_isData.ts | 1.83 kB ( |
| STATIC_tgpu.ts | 276.85 kB ( |
| tgpu_fn.ts | 276.86 kB ( |
| tgpu_init.ts | 276.86 kB ( |
| tgpu_lazy.ts | 276.86 kB ( |
| tgpu_slot.ts | 276.86 kB ( |
| tgpu_const.ts | 276.86 kB ( |
| tgpu_unroll.ts | 276.86 kB ( |
| tgpu_resolve.ts | 276.86 kB ( |
| tgpu_accessor.ts | 276.86 kB ( |
| tgpu_comptime.ts | 276.86 kB ( |
| tgpu_vertexFn.ts | 276.86 kB ( |
| tgpu_computeFn.ts | 276.86 kB ( |
| tgpu_fragmentFn.ts | 276.86 kB ( |
| tgpu_privateVar.ts | 276.86 kB ( |
| tgpu_vertexLayout.ts | 276.87 kB ( |
| tgpu_workgroupVar.ts | 276.87 kB ( |
| tgpu_initFromDevice.ts | 276.87 kB ( |
| tgpu_bindGroupLayout.ts | 276.87 kB ( |
| tgpu_mutableAccessor.ts | 276.87 kB ( |
| tgpu_resolveWithContext.ts | 276.87 kB ( |
| d_deepEqual.ts | 2.22 kB ( |
| STATIC_allImports.ts | 302.69 kB ( |
| d_bool.ts | 13.89 kB ( |
| d_f16.ts | 13.89 kB ( |
| d_f32.ts | 13.89 kB ( |
| d_i32.ts | 13.89 kB ( |
| d_u32.ts | 13.89 kB ( |
| d_u16.ts | 13.91 kB ( |
| d_textureDepth2d.ts | 14.33 kB ( |
| d_textureDepthCube.ts | 14.34 kB ( |
| d_texture1d.ts | 14.34 kB ( |
| d_texture2d.ts | 14.34 kB ( |
| d_texture3d.ts | 14.34 kB ( |
| d_textureCube.ts | 14.35 kB ( |
| d_textureDepth2dArray.ts | 14.35 kB ( |
| d_textureDepthCubeArray.ts | 14.36 kB ( |
| d_textureDepthMultisampled2d.ts | 14.36 kB ( |
| d_texture2dArray.ts | 14.36 kB ( |
| d_textureCubeArray.ts | 14.37 kB ( |
| d_textureMultisampled2d.ts | 14.37 kB ( |
| std_discard.ts | 15.18 kB ( |
| std_isBeingTranspiled.ts | 15.28 kB ( |
| std_getTargetShaderLanguage.ts | 15.34 kB ( |
| std_extensionEnabled.ts | 15.40 kB ( |
| std_copy.ts | 15.42 kB ( |
| std_arrayLength.ts | 15.43 kB ( |
| std_range.ts | 15.66 kB ( |
| d_disarrayOf.ts | 15.83 kB ( |
| std_dpdx.ts | 16.14 kB ( |
| std_dpdxCoarse.ts | 16.15 kB ( |
| std_dpdxFine.ts | 16.15 kB ( |
| std_dpdy.ts | 16.15 kB ( |
| std_dpdyCoarse.ts | 16.15 kB ( |
| std_dpdyFine.ts | 16.15 kB ( |
| std_fwidth.ts | 16.15 kB ( |
| std_fwidthCoarse.ts | 16.15 kB ( |
| std_fwidthFine.ts | 16.15 kB ( |
| std_atomicLoad.ts | 16.94 kB ( |
| std_atomicStore.ts | 16.95 kB ( |
| std_textureBarrier.ts | 16.95 kB ( |
| std_atomicAdd.ts | 16.96 kB ( |
| std_atomicAnd.ts | 16.96 kB ( |
| std_atomicMax.ts | 16.96 kB ( |
| std_atomicMin.ts | 16.96 kB ( |
| std_atomicOr.ts | 16.96 kB ( |
| std_atomicSub.ts | 16.96 kB ( |
| std_atomicXor.ts | 16.96 kB ( |
| std_storageBarrier.ts | 16.96 kB ( |
| std_workgroupBarrier.ts | 16.96 kB ( |
| d_vec2b.ts | 20.34 kB ( |
| d_vec2f.ts | 20.34 kB ( |
| d_vec2h.ts | 20.34 kB ( |
| d_vec2i.ts | 20.34 kB ( |
| d_vec2u.ts | 20.34 kB ( |
| d_vec3b.ts | 20.34 kB ( |
| d_vec3f.ts | 20.34 kB ( |
| d_vec3h.ts | 20.34 kB ( |
| d_vec3i.ts | 20.34 kB ( |
| d_vec3u.ts | 20.34 kB ( |
| d_vec4b.ts | 20.34 kB ( |
| d_vec4f.ts | 20.34 kB ( |
| d_vec4h.ts | 20.34 kB ( |
| d_vec4i.ts | 20.34 kB ( |
| d_vec4u.ts | 20.34 kB ( |
| d_isInvariantAttrib.ts | 784 B |
import { ... } in PR vs import * as ... in PR (is the library tree-Shakeable?):
| Test | tsdown |
|---|---|
| tgpu_init.ts | 267.77 kB ( |
| tgpu_initFromDevice.ts | 267.22 kB ( |
| tgpu_resolve.ts | 168.34 kB ( |
| tgpu_resolveWithContext.ts | 168.27 kB ( |
| tgpu_bindGroupLayout.ts | 73.79 kB ( |
| tgpu_mutableAccessor.ts | 68.51 kB ( |
| tgpu_accessor.ts | 68.51 kB ( |
| tgpu_privateVar.ts | 67.07 kB ( |
| tgpu_workgroupVar.ts | 67.07 kB ( |
| tgpu_const.ts | 66.62 kB ( |
| tgpu_lazy.ts | 66.42 kB ( |
| tgpu_fragmentFn.ts | 38.92 kB ( |
| tgpu_fn.ts | 38.86 kB ( |
| tgpu_vertexFn.ts | 38.73 kB ( |
| tgpu_computeFn.ts | 38.44 kB ( |
| tgpu_vertexLayout.ts | 27.57 kB ( |
| tgpu_comptime.ts | 15.17 kB ( |
| tgpu_unroll.ts | 1.75 kB ( |
| tgpu_slot.ts | 1.70 kB ( |
If you wish to run a comparison for other, slower bundlers, run the 'Tree-shake test' from the GitHub Actions menu.
There was a problem hiding this comment.
This is a solid, well-tested addition of the cross-runtime transfer protocol. The snapshot/restore split is clean, the tests cover all the major resource kinds, and the JSI-specific type guards are a nice touch for React Native.
I have a few small suggestions around TgpuBindGroupImpl / bind-group restoration that are worth double-checking before merge, but nothing that blocks the overall approach. The relevant unit tests (serial, serializeDataSchema, computePipeline, renderPipeline, internal/typeGuards, root) all pass locally.
Kimi K2 (free via Pullfrog for OSS) | 𝕏
| this[$soul] = { | ||
| type: 'bind-group', | ||
| // Undefined only in rootless `tgpu.resolve()`, where the group is never unwrapped | ||
| device: root?.device as GPUDevice, |
There was a problem hiding this comment.
This as GPUDevice hides the fact that device can be undefined when the bind group is created rootlessly via tgpu.resolve(). TgpuDeviceOwningSoul requires device: GPUDevice, so the type system will treat this as a real device even when it isn't. Consider making device optional in the soul type for rootless resources, or using a separate soul shape, so the runtime value matches the type.
| for (const key of Object.keys(layout.entries)) { | ||
| if (layout.entries[key] !== null && !(key in entries)) { | ||
| throw new MissingBindingError(getName(layout), key); | ||
| if (!raw) { |
There was a problem hiding this comment.
Skipping the missing-entry check when raw is provided makes sense for restored/materialized bind groups, but it means the constructor now allows bind groups with incomplete entries as long as a raw GPUBindGroup is passed. Since TgpuBindGroupImpl is exported, this is a bit of a footgun. Could you add a clarifying comment that this path is only for restoration, or consider validating anyway when raw comes from an untrusted source?
75cdd1a to
1177c63
Compare
There was a problem hiding this comment.
Pull request overview
Implements a snapshot/restore (“transfer”) protocol for TypeGPU resources built on top of souls, enabling React Native worklet runtimes to receive precompiled pipelines and resource wrappers while keeping runtime-local definitions (functions/comptime) non-transferable.
Changes:
- Add core serialization infrastructure: resource snapshotting/restoration, data-schema serialization, and vec/mat value snapshots.
- Enable restoration of precompiled compute/render pipelines (no re-resolution) and broaden public type guards/exports to support transfer use-cases.
- Integrate React Native worklets support in
@typegpu/react(UI-threaduseFrame, automatic custom-serializable registration, identity cache) plus docs/tests.
Reviewed changes
Copilot reviewed 39 out of 40 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| pnpm-lock.yaml | Dependency graph updates (RN worklets/metro/babel). |
| packages/typegpu/tests/serializeDataSchema.test.ts | Tests for schema serialization + round-trip. |
| packages/typegpu/tests/serial.test.ts | End-to-end resource snapshot/restore tests. |
| packages/typegpu/tests/root.test.ts | Verifies optionalFeatures handling during init. |
| packages/typegpu/tests/renderPipeline.test.ts | Tests restoring raw render pipelines + bind groups. |
| packages/typegpu/tests/internal/typeGuards.test.ts | Tests JSI-hostobject-safe pipeline type guards. |
| packages/typegpu/tests/computePipeline.test.ts | Tests restoring raw compute pipelines + bind groups. |
| packages/typegpu/src/tgpuBindGroupLayout.ts | Bind-group souls/restoration + materialization changes. |
| packages/typegpu/src/std/bitcast.ts | Import correction for getName. |
| packages/typegpu/src/serial/types.ts | Introduces RestoreContext interface. |
| packages/typegpu/src/serial/schema.ts | Serialize/deserialize data schemas. |
| packages/typegpu/src/serial/restore.ts | Central soul→resource restoration registry. |
| packages/typegpu/src/serial/registry.ts | Snapshot/restore entrypoints + schema tagging. |
| packages/typegpu/src/serial/dataValue.ts | Snapshot/restore vec/mat instances via bytes. |
| packages/typegpu/src/resolutionCtx.ts | Catchall bind-group creation updated for new ctor. |
| packages/typegpu/src/internal.ts | Re-export serialization APIs for ~internal. |
| packages/typegpu/src/indexNamedExports.ts | Export new guards/types (bind groups, pipelines, etc.). |
| packages/typegpu/src/data/index.ts | Export invariant helpers/types. |
| packages/typegpu/src/core/texture/texture.ts | Allow wrapping existing GPUTexture; avoid double-destroy. |
| packages/typegpu/src/core/root/init.ts | Init optionalFeatures handling + bind-group creation update. |
| packages/typegpu/src/core/pipeline/renderPipeline.ts | Precompiled render pipeline restoration support. |
| packages/typegpu/src/core/pipeline/pipelineUtils.ts | Helpers to collect priors + restore timestamps. |
| packages/typegpu/src/core/pipeline/computePipeline.ts | Precompiled compute pipeline restoration support. |
| packages/typegpu/src/core/buffer/buffer.ts | Helper to reapply buffer usages on restore. |
| packages/typegpu-react/tsdown.config.ts | Bundler plugin to preserve optional require() for Metro. |
| packages/typegpu-react/tests/root-context.test.tsx | Ensures Root options flow into init. |
| packages/typegpu-react/tests/react-native/use-frame.test.tsx | Tests UI-vs-RN thread useFrame behavior. |
| packages/typegpu-react/tests/react-native/register-serializables.test.ts | Tests RN custom-serializable registration + transfer. |
| packages/typegpu-react/src/react-native/worklets-integration.ts | Optional runtime detection of react-native-worklets. |
| packages/typegpu-react/src/react-native/use-frame.ts | UI-runtime frame loop for worklet callbacks. |
| packages/typegpu-react/src/react-native/use-configure-worklet-context.ts | Shareable canvas context ref for UI runtime. |
| packages/typegpu-react/src/react-native/serialization/transfer-cache.ts | Identity cache for transferred resources/roots. |
| packages/typegpu-react/src/react-native/serialization/register-serializables.ts | Custom serializer pack/unpack using snapshot protocol. |
| packages/typegpu-react/src/react-native/index.ts | RN entry: install WebGPU + register serializables + export hooks. |
| packages/typegpu-react/src/core/use-frame.ts | Extract shared startFrameLoop helper + export FrameCtx. |
| packages/typegpu-react/src/core/root-context.tsx | Add init options passthrough + disableWorklets flag/context. |
| packages/typegpu-react/README.md | Document RN worklets support. |
| packages/typegpu-react/package.json | Add RN export conditions + optional worklets peer. |
| apps/typegpu-docs/src/content/docs/integration/react-native/worklets.mdx | New RN Worklets guide. |
| apps/typegpu-docs/astro.config.mjs | Add docs sidebar entry for worklets guide. |
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| public unwrap(unwrapper: Unwrapper): GPUBindGroup { | ||
| const raw = this[$soul].raw; | ||
| if (raw) { | ||
| return raw; | ||
| } | ||
|
|
||
| const unwrapped = unwrapper.device.createBindGroup({ | ||
| label: getName(this.layout) ?? '<unnamed>', | ||
| layout: unwrapper.unwrap(this.layout), |
1177c63 to
b589181
Compare
b589181 to
4ab25cb
Compare
There was a problem hiding this comment.
ℹ️ No new issues found in the latest push.
Reviewed changes
The latest force-push is a small cleanup delta:
- Moved the root-class JSDoc comment in
packages/typegpu/src/core/root/init.tsontoTgpuRootImpl. - Updated
packages/typegpu/src/shared/soul.tsandpackages/typegpu/src/shared/symbols.tsdocstrings to describe souls as surviving transfer between runtimes. - Wired resource names into souls in
packages/typegpu/src/shared/meta.ts:getNamenow falls back tosoul.label, andsetNamewrites the name into the soul so labels survive cross-runtime transfer.
The relevant unit tests still pass.
Kimi K2 (free via Pullfrog for OSS) | 𝕏

Replaces #2732, rebuilt on top of #2796. With souls in place the protocol got much simpler: a snapshot is just a structured copy of the resource's soul
tgpu.fn, entry functions,tgpu.comptime) stay runtime-local